const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const glob = require('glob');
const webpackCommonConfig = require('../build/webpack.common.js');// 我们项目中的配置，公司项目代码无法上传
const alias = webpackCommonConfig().resolve.alias;// 获取到页面中的别名
const postcss = require('postcss');
const postcssScss = require('postcss-sass');

// 后缀 优先级ts比js高
const EXTS = ['.ts', '.tsx', '.js', '.jsx', '.json'];

// 别名都得单独处理,因为无法判断各个项目中的别名规则
function handleAlias(modulePath) {
    // 区分@/ @components @redux
    const isShortAlias = modulePath.startsWith('@/');

    for (let key in alias) {
        if (modulePath.indexOf(key) > -1) {
            if (key === '@' && isShortAlias) {
                modulePath = modulePath.replace(key, alias[key]);
                break;
            } else if (['@components', '@redux'].includes(key) && !isShortAlias) {
                modulePath = modulePath.replace(key, alias[key]);
                break;
            } else if (['$bere', '@tencent/bere-form', '@tencent/bere-hoc', '@tencent/bere-utils-function'].includes(key)) {
                modulePath = modulePath.replace(key, alias[key]);
            }
        }
    }

    return modulePath;
}

/**
 * 补全路径，找不到默认为node_modules下的文件，不做处理,处理别名
 * @param {*} modulePath string
 * @returns 完整路径 pathString
 */
function handleCompletePath(modulePath, needHandleAlias = true) {

    let result = (function () {
        if (needHandleAlias) {
            // 处理别名
            modulePath = handleAlias(modulePath);
        }

        // 添加后缀，1.可能代码省略了后缀，2.只写到文件夹，默认去找index.js或者index.tsx
        if (modulePath.match(/\.[a-zA-Z]+$/)) {
            return modulePath;
        }

        function tryCompletePath(resolvePath) {
            for (let i = 0; i < EXTS.length; i++) {
                let tryPath = resolvePath(EXTS[i]);
                if (fs.existsSync(tryPath)) {
                    return tryPath;
                }
            }
        }

        try {
            // 如果找不到文件会报错，所以用try catch写了
            var stat = fs.statSync(modulePath);
            if (stat.isDirectory()) {
                const tryModulePath = tryCompletePath((ext) => path.join(modulePath, 'index' + ext));
                if (!tryModulePath) {
                    return ''
                } else {
                    return tryModulePath;
                }
            }
        } catch (err) {
            if (!EXTS.some(ext => modulePath.endsWith(ext))) {
                const tryModulePath = tryCompletePath((ext) => modulePath + ext);
                if (!tryModulePath) {
                    return ''
                } else {
                    return tryModulePath;
                }
            }
        }

        return modulePath;
    })();
    return result.replace(/\\/g, '/');

}

/**
 * 专门处理js ts文件的traverse
 * @param {*} file 文件路径
 * @param {*} deps 依赖
 * @param {*} entryDirname 入口文件目录 
 */
function traverseJsModule(file, deps, entryDirname, cache) {
    const body = fs.readFileSync(file, 'utf-8');

    const ast = parser.parse(body, {
        sourceType: 'module',// 使用es6的module
        plugins: ["jsx", "typescript", "tsx", "decorators-legacy", "classProperties", "asyncGenerators", "asyncDoExpressions"],// 根据报错查看babel官网得出的解决方案 https://www.babeljs.cn/docs/babel-parser
    })

    traverse(ast, {
        ImportDeclaration({ node }) {
            // 拿到file的目录，根据这个路径去resove,解决相对路径和绝对路径的问题
            const dirname = path.dirname(file);
            let astpath = '';
            let aliasBol = false;

            Object.keys(alias).map(key => {
                if (node.source.value.indexOf(key) > -1) {
                    astpath = node.source.value;
                    aliasBol = true;
                }
            })

            // 别名自带resolve，所以不再需要resolve
            if (!aliasBol) {
                astpath = path.resolve(dirname, node.source.value).replace(/\\/g, '/');
            }

            const completePath = handleCompletePath(astpath, aliasBol);
            if (completePath) {
                // 如果找到了文件可以收集依赖，找不到文件，可能是在node_modules中的
                deps.push(astpath);
            }
        },
        CallExpression(callInfo) {
            // route 路由页面,用import()方式加载页面
            if (callInfo.get('callee').toString() === 'import') {
                let route = path.join(entryDirname, 'routes', callInfo.node.arguments[0].value)
                // 这里面没有别名,无需特殊处理
                let needHandleAlias = false;
                const completePath = handleCompletePath(route, needHandleAlias);
                deps.push(completePath);
            }

            if (callInfo.get('callee').toString() === 'require') {

                let path = callInfo.node.arguments[0].value
                if (path === 'echarts/map/js/china.js') {
                    return
                }

                let completePath = handleCompletePath(path);
                cache[completePath] = 1;// 用于记录曾访问过
            }
        }
    })
}

/**
 * 专门处理css文件的traverse
 * @param {*} file 文件路径
 * @param {*} deps 依赖
 * @param {*} entryDirname 入口文件目录 
 */
function traverseCssModule(file, deps, entryDirname, cache) {
    // 坑，不支持注释   data:image ///  https://
    const body = fs.readFileSync(file, 'utf-8');
    // 解决data:image
    if (body.indexOf('///') > -1) {
        return ''
    }

    // scss代码中有注释，postcss工具不支持
    newbody = body.replace(/(?<!:)\/\/(?!\/).*/g, '')

    let ast = postcss.parse(newbody, {
        syntaxt: postcssScss,
    })

    ast.walkDecls(obj => {
        // background 中的url
        if (obj.value.includes('url(')) {
            const url = /.*url\((.+)\).*/.exec(obj.value)[1].replace(/['"]/g, '');

            // 过滤掉网络加载的图片
            if (url.indexOf('http') === -1) {
                let completePath = entryDirname + '/assets' + url.split('assets')[1];
                completePath = completePath.replace(/\\/g, '/');
                cache[completePath] = 1;
            }
        }
    })

    ast.walkAtRules('import', rule => {
        const dirname = path.dirname(file);
        let scssName = rule.params.replace(/['"]/g, '');
        scssName = path.resolve(dirname, scssName)
        completePath = handleCompletePath(scssName)
        cache[completePath] = 1;
    });
}

/**
 * 单独分析逐个模块信息
 * @param {*} file 路径string
 * @param {*} cache 用来记录访问过哪些文件，以及解决循环引用问题
 * @returns { filename: filename, deps } 文件名和依赖
 */
function getModuleInfo(file, cache, entryDirname) {
    // 拼接完整路径
    file = handleCompletePath(file);

    // 处理循环依赖
    if (cache[file]) {
        return '';
    } else {
        cache[file] = 1;
    }

    const extname = path.extname(file);
    const deps = [];
    if (['.js', 'ts', '.tsx', '.jsx', 'json'].includes(extname)) {
        traverseJsModule(file, deps, entryDirname, cache);
    }

    if (['.scss', 'css'].includes(extname)) {
        traverseCssModule(file, deps, entryDirname, cache)
    }

    const filename = path.resolve(__dirname, file).replace(/\\/g, '/');
    const moduleInfo = { filename: filename, deps };

    return moduleInfo
}
// 获取依赖
// 递归调用 -》引用的资源还会引用其他的资源
function getDeps({ deps }, cache, entryDirname) {
    deps.forEach(key => {
        const child = getModuleInfo(key, cache, entryDirname);
        if (child) {
            getDeps(child, cache, entryDirname)
        }
    });
}

/**
 * 
 * @param {*} file string 文件路径
 * @param {*} entryDirname 入口文件的目录
 */
function parseModules(file, entryDirname) {
    const usedFiles = {};// 其实相当于缓存
    // 先分析出入口文件的依赖，收集之后，去递归依赖
    const moduleInfo = getModuleInfo(file, usedFiles, entryDirname);

    // 收集依赖
    getDeps(moduleInfo, usedFiles, entryDirname);

    return usedFiles
}

/**
 * 获取路径下所有的模块
 */
function getAllFiles(pattern) {
    return new Promise((resolve, reject) => {
        glob(pattern, {
            nodir: true
        }, (err, files) => {
            const ret = files.map(item => {
                // windows 下面主意路径问题
                return path.resolve(process.cwd(), item).replace(/\\/g, '/')
            })
            resolve(ret)
        })
    })
}

const findUnusedFile = async function ({ entry, searchAllFilesPattern, aliasObj }) {
    // 路径比对用的绝对毕竟比对，用__dirname去拼接
    const allFiles = await getAllFiles(searchAllFilesPattern);
    const dirname = path.dirname(entry)
    // 解析模块 内部会递归
    const usedFiles = parseModules(entry, dirname);

    // 这样写性能消耗太大
    // const unusedFiles = allFiles.filter((file) => Object.keys(usedFiles).includes(file));
    const unusedFiles = [];
    const len = allFiles.length;

    // 所有文件中排除访问过的文件就是没有访问过的文件
    for (let i = 0; i < len; i++) {
        let filename = allFiles[i]
        if (usedFiles[filename]) {
            continue
        } else {
            unusedFiles.push(filename);
        }
    }

    fs.writeFileSync('./unusedFiles.json', JSON.stringify(unusedFiles))

    // 其实这里应该有删除文件的脚本，怕错删，项目中就手动删除优化

    return unusedFiles
}


let config = {
    entry: path.resolve(process.cwd(), '../src/index.js'),// 入口文件
    searchAllFilesPattern: '../src/**/*',// 查询所有文件
    alias,// webpack中设置的别名
}

findUnusedFile(config);